home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2010 May / Mac Life Ubuntu.iso / casper / filesystem.squashfs / usr / share / pyshared / apport / packaging_impl.py < prev    next >
Encoding:
Python Source  |  2009-09-25  |  34.9 KB  |  931 lines

  1. '''An apport.PackageInfo class implementation for python-apt and dpkg, as found
  2. on Debian and derivatives such as Ubuntu.
  3.  
  4. Copyright (C) 2007, 2009 Canonical Ltd.
  5. Author: Martin Pitt <martin.pitt@ubuntu.com>
  6.  
  7. This program is free software; you can redistribute it and/or modify it
  8. under the terms of the GNU General Public License as published by the
  9. Free Software Foundation; either version 2 of the License, or (at your
  10. option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
  11. the full text of the license.
  12. '''
  13.  
  14. import subprocess, os, glob, stat, sys, tempfile, glob, re, shutil
  15.  
  16. import warnings
  17. warnings.filterwarnings('ignore', 'apt API not stable yet', FutureWarning)
  18. import apt
  19.  
  20. from apport.packaging import PackageInfo
  21.  
  22. class __AptDpkgPackageInfo(PackageInfo):
  23.     '''Concrete apport.PackageInfo class implementation for python-apt and
  24.     dpkg, as found on Debian and derivatives such as Ubuntu.'''
  25.  
  26.     def __init__(self):
  27.         self._apt_cache = None
  28.         self._contents_dir = None
  29.         self._mirror = None
  30.  
  31.         self.configuration = '/etc/default/apport'
  32.  
  33.     def __del__(self):
  34.         try:
  35.             if self._contents_dir:
  36.                 import shutil
  37.                 shutil.rmtree(self._contents_dir)
  38.         except AttributeError:
  39.             pass
  40.  
  41.     def _cache(self, package):
  42.         '''Return apt.Cache()[package] (initialized lazily).
  43.         
  44.         Throw a ValueError if the package does not exist.'''
  45.  
  46.         if not self._apt_cache:
  47.             self._apt_cache = apt.Cache()
  48.         try:
  49.             return self._apt_cache[package]
  50.         except KeyError:
  51.             raise ValueError, 'package does not exist'
  52.  
  53.     def get_version(self, package):
  54.         '''Return the installed version of a package.'''
  55.  
  56.         return self._cache(package).installedVersion
  57.  
  58.     def get_available_version(self, package):
  59.         '''Return the latest available version of a package.'''
  60.  
  61.         return self._cache(package).candidateVersion
  62.  
  63.     def get_dependencies(self, package):
  64.         '''Return a list of packages a package depends on.'''
  65.  
  66.         cur_ver = self._cache(package)._pkg.CurrentVer
  67.         if not cur_ver:
  68.             # happens with virtual packages
  69.             return []
  70.         return [d[0].TargetPkg.Name for d in cur_ver.DependsList.get('Depends', []) +
  71.             cur_ver.DependsList.get('PreDepends', [])]
  72.  
  73.     def get_source(self, package):
  74.         '''Return the source package name for a package.'''
  75.  
  76.         return self._cache(package).sourcePackageName
  77.  
  78.     def is_distro_package(self, package):
  79.         '''Check if a package is a genuine distro package (True) or comes from
  80.         a third-party source.'''
  81.  
  82.         lsb_release = subprocess.Popen(['lsb_release', '-i', '-s'],
  83.             stdout=subprocess.PIPE)
  84.         this_os = lsb_release.communicate()[0].strip()
  85.         assert lsb_release.returncode == 0
  86.  
  87.         if self._cache(package).installedVersion is None:
  88.             return False # LP#252734
  89.  
  90.         origins = self._cache(package).candidateOrigin
  91.         if origins: # might be None
  92.             for o in origins:
  93.                 # note: checking site for ppa is a hack until LP #140412 gets fixed
  94.                 if o.origin == this_os and not o.site.startswith('ppa'):
  95.                     return True
  96.         return False
  97.  
  98.     def get_architecture(self, package):
  99.         '''Return the architecture of a package.
  100.  
  101.         This might differ on multiarch architectures (e. g.  an i386 Firefox
  102.         package on a x86_64 system)'''
  103.  
  104.         return self._cache(package).architecture or 'unknown'
  105.  
  106.     def get_files(self, package):
  107.         '''Return list of files shipped by a package.'''
  108.  
  109.         list = self._call_dpkg(['-L', package])
  110.         if list is None:
  111.             return None
  112.         return [f for f in list.splitlines() if not f.startswith('diverted')]
  113.  
  114.     def get_modified_files(self, package):
  115.         '''Return list of all modified files of a package.'''
  116.  
  117.         # get the maximum mtime of package files that we consider unmodified
  118.         listfile = '/var/lib/dpkg/info/%s.list' % package
  119.         try:
  120.             s = os.stat(listfile)
  121.             if not stat.S_ISREG(s.st_mode):
  122.                 raise OSError
  123.             max_time = max(s.st_mtime, s.st_ctime)
  124.         except OSError:
  125.             return [listfile]
  126.  
  127.         # create a list of files with a newer timestamp for md5sum'ing
  128.         sums = ''
  129.         sumfile = '/var/lib/dpkg/info/%s.md5sums' % package
  130.         # some packages do not ship md5sums, shrug on them
  131.         if not os.path.exists(sumfile):
  132.             return []
  133.  
  134.         for line in open(sumfile):
  135.             try:
  136.                 # ignore lines with NUL bytes (happens, LP#96050)
  137.                 if '\0' in line:
  138.                     print >> sys.stderr, 'WARNING:', sumfile, 'contains NUL character, ignoring line'
  139.                     continue
  140.                 words  = line.split()
  141.                 if len(line) < 1:
  142.                     print >> sys.stderr, 'WARNING:', sumfile, 'contains empty line, ignoring line'
  143.                     continue
  144.                 s = os.stat('/' + words[-1])
  145.                 if max(s.st_mtime, s.st_ctime) <= max_time:
  146.                     continue
  147.             except OSError:
  148.                 pass
  149.  
  150.             sums += line
  151.  
  152.         if sums:
  153.             return self._check_files_md5(sums)
  154.         else:
  155.             return []
  156.  
  157.     def __fgrep_files(self, pattern, file_list):
  158.         '''Call fgrep for a pattern on given file list and return the first
  159.         matching file, or None if no file matches.'''
  160.  
  161.         match = None
  162.         slice_size = 100
  163.         i = 0
  164.  
  165.         while not match and i < len(file_list):
  166.             p = subprocess.Popen(['fgrep', '-lxm', '1', '--', pattern] +
  167.                 file_list[i:i+slice_size], stdin=subprocess.PIPE,
  168.                 stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
  169.             out = p.communicate()[0]
  170.             if p.returncode == 0:
  171.                 match = out
  172.             i += slice_size
  173.  
  174.         return match
  175.  
  176.     def get_file_package(self, file, uninstalled=False, map_cachedir=None):
  177.         '''Return the package a file belongs to, or None if the file is not
  178.         shipped by any package.
  179.         
  180.         If uninstalled is True, this will also find files of uninstalled
  181.         packages; this is very expensive, though, and needs network access and
  182.         lots of CPU and I/O resources. In this case, map_cachedir can be set to
  183.         an existing directory which will be used to permanently store the
  184.         downloaded maps. If it is not set, a temporary directory will be used.'''
  185.  
  186.         # check if the file is a diversion
  187.         dpkg = subprocess.Popen(['/usr/sbin/dpkg-divert', '--list', file],
  188.             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  189.         out = dpkg.communicate()[0]
  190.         if dpkg.returncode == 0 and out:
  191.             return out.split()[-1]
  192.  
  193.         fname = os.path.splitext(os.path.basename(file))[0].lower()
  194.  
  195.         all_lists = []
  196.         likely_lists = []
  197.         for f in glob.glob('/var/lib/dpkg/info/*.list'):
  198.             p = os.path.splitext(os.path.basename(f))[0].lower()
  199.             if p in fname or fname in p:
  200.                 likely_lists.append(f)
  201.             else:
  202.                 all_lists.append(f)
  203.  
  204.         # first check the likely packages
  205.         match = self.__fgrep_files(file, likely_lists)
  206.         if not match:
  207.             match = self.__fgrep_files(file, all_lists)
  208.  
  209.         if match:
  210.             return os.path.splitext(os.path.basename(match))[0]
  211.  
  212.         if uninstalled:
  213.             return self._search_contents(file, map_cachedir)
  214.         else:
  215.             return None
  216.  
  217.     def get_system_architecture(self):
  218.         '''Return the architecture of the system, in the notation used by the
  219.         particular distribution.'''
  220.  
  221.         dpkg = subprocess.Popen(['dpkg', '--print-architecture'],
  222.             stdout=subprocess.PIPE)
  223.         arch = dpkg.communicate()[0].strip()
  224.         assert dpkg.returncode == 0
  225.         assert arch
  226.         return arch
  227.  
  228.     def set_mirror(self, url):
  229.         '''Explicitly set a distribution mirror URL for operations that need to
  230.         fetch distribution files/packages from the network.
  231.  
  232.         By default, the mirror will be read from the system configuration
  233.         files.'''
  234.  
  235.         self._mirror = url
  236.  
  237.     def get_source_tree(self, srcpackage, dir, version=None):
  238.         '''Download given source package and unpack it into dir (which should
  239.         be empty).
  240.  
  241.         This also has to care about applying patches etc., so that dir will
  242.         eventually contain the actually compiled source.
  243.  
  244.         If version is given, this particular version will be retrieved.
  245.         Otherwise this will fetch the latest available version.
  246.  
  247.         Return the directory that contains the actual source root directory
  248.         (which might be a subdirectory of dir). Return None if the source is
  249.         not available.'''
  250.  
  251.         # fetch source tree
  252.         argv = ['apt-get', '--assume-yes', 'source', srcpackage]
  253.         if version:
  254.             argv[-1] += '=' + version
  255.         try:
  256.             if subprocess.call(argv, stdout=subprocess.PIPE,
  257.                 cwd=dir) != 0:
  258.                 return None
  259.         except OSError:
  260.             return None
  261.  
  262.         # find top level directory
  263.         root = None
  264.         for d in glob.glob(os.path.join(dir, srcpackage + '-*')):
  265.             if os.path.isdir(d):
  266.                 root = d
  267.         assert root, 'could not determine source tree root directory'
  268.  
  269.         # apply patches on a best-effort basis 
  270.         try:
  271.             subprocess.call('debian/rules patch || debian/rules apply-patches ' \
  272.                 '|| debian/rules apply-dpatches || '\
  273.                 'debian/rules unpack || debian/rules patch-stamp || ' \
  274.                 'debian/rules setup', shell=True, cwd=root,
  275.                 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  276.         except OSError:
  277.             pass
  278.  
  279.         return root
  280.  
  281.     def get_kernel_package(self):
  282.         '''Return the actual Linux kernel package name.
  283.  
  284.         This is used when the user reports a bug against the "linux" package.
  285.         '''
  286.         # TODO: Ubuntu specific
  287.         return 'linux-image-' + os.uname()[2]
  288.  
  289.     def install_retracing_packages(self, report, verbosity=0,
  290.             unpack_only=False, no_pkg=False, extra_packages=[]):
  291.         '''Install packages which are required to retrace a report.
  292.         
  293.         If package installation fails (e. g. because the user does not have root
  294.         privileges), the list of required packages is printed out instead.
  295.  
  296.         If unpack_only is True, packages are only temporarily unpacked and
  297.         purged again after retrace, instead of permanently and fully installed.
  298.         If no_pkg is True, the package manager is not used at all, but the
  299.         binary packages are just unpacked with low-level tools; this speeds up
  300.         operations in fakechroots, but makes it impossible to cleanly remove
  301.         the package, so only use that in apport-chroot.
  302.         
  303.         Return a tuple (list of installed packages, string with outdated packages).
  304.         '''
  305.         self._cache('bash') # ensure that cache is initialized
  306.         c = self._apt_cache
  307.  
  308.         try:
  309.             if verbosity:
  310.                 c.update(apt.progress.TextFetchProgress())
  311.             else:
  312.                 c.update()
  313.             c.open(apt.progress.OpProgress())
  314.         except SystemError, e:
  315.             if 'Hash Sum mismatch' in str(e):
  316.                 # temporary archive inconsistency
  317.                 print >> sys.stderr, str(e), 'aborting'
  318.                 sys.exit(99) # signal crash digger about transient error
  319.             else:
  320.                 raise
  321.         except apt.cache.LockFailedException:
  322.             if os.geteuid() != 0:
  323.                 print >> sys.stderr, 'WARNING: Could not update apt, you need to be root'
  324.             else:
  325.                 raise
  326.  
  327.         installed = []
  328.         uninstallable = []
  329.         outdated = ''
  330.  
  331.         # create map of dependency package versions as specified in report
  332.         dependency_versions = {}
  333.         for l in (report['Package'] + '\n' + report.get('Dependencies', '')).splitlines():
  334.             if not l.strip():
  335.                 continue
  336.             (pkg, version) = l.split()[:2]
  337.             dependency_versions[pkg] = version
  338.             try:
  339.                 # this fails for packages which are still installed, but gone from
  340.                 # the archive; i. e. /var/lib/dpkg/status still knows about them
  341.                 if not c[pkg]._lookupRecord():
  342.                     raise KeyError
  343.                 if not 'Architecture: all' in c[pkg]._records.Record:
  344.                     dependency_versions[pkg+'-dbgsym'] = dependency_versions[pkg]
  345.             except KeyError:
  346.                 print >> sys.stderr, 'WARNING: package %s not known to package cache' % pkg
  347.  
  348.         for pkg, ver in dependency_versions.iteritems():
  349.             if not c.has_key(pkg):
  350.                 print >> sys.stderr, 'WARNING: package %s not available' % pkg
  351.                 continue
  352.  
  353.             # ignore packages which are already installed in the right version
  354.             if (ver and c[pkg].installedVersion == ver) or \
  355.                (not ver and c[pkg].installedVersion):
  356.                continue
  357.  
  358.             if ver and c[pkg].candidateVersion != ver:
  359.                 if not pkg.endswith('-dbgsym'):
  360.                     outdated += '%s: installed version %s, latest version: %s\n' % (
  361.                         pkg, ver, c[pkg].candidateVersion)
  362.                 print >> sys.stderr, 'WARNING: %s version %s required, but %s is available' % (
  363.                     pkg, ver, c[pkg].candidateVersion)
  364.                 if not unpack_only:
  365.                     uninstallable.append (c[pkg].name)
  366.                     continue
  367.  
  368.             c[pkg].markInstall(False)
  369.  
  370.         # extra packages
  371.         for p in extra_packages:
  372.             c[p].markInstall(False)
  373.  
  374.         if verbosity:
  375.             fetchProgress = apt.progress.TextFetchProgress()
  376.             installProgress = apt.progress.InstallProgress()
  377.         else:
  378.             fetchProgress = apt.progress.FetchProgress()
  379.             installProgress = apt.progress.DumbInstallProgress()
  380.  
  381.         try:
  382.             if c.getChanges():
  383.                 os.environ['DEBIAN_FRONTEND'] = 'noninteractive'
  384.                 if unpack_only:
  385.                     self.fetch_unpack(c, fetchProgress, no_pkg, verbosity)
  386.                 else:
  387.                     try:
  388.                         c.commit(fetchProgress, installProgress)
  389.                     except SystemError:
  390.                         print >> sys.stderr, 'Error: Could not install all archives. If you use this tool on a production system, it is recommended to use the -u option. See --help for details.'
  391.                         sys.exit(1)
  392.  
  393.                 # after commit(), the Cache object does not empty the pending
  394.                 # changes, so we need to reinitialize it to avoid applying the same
  395.                 # changes again below
  396.                 installed = [p.name for p in c.getChanges()]
  397.                 c = apt.Cache()
  398.         except IOError, e:
  399.             pass # we will complain to the user later
  400.  
  401.         # check list of libraries that the crashed process referenced at
  402.         # runtime and warn about those which are not available
  403.         libs = set()
  404.         if report.has_key('ProcMaps'):
  405.             for l in report['ProcMaps'].splitlines():
  406.                 if not l.strip():
  407.                     continue
  408.                 cols = l.split()
  409.                 if 'x' in cols[1] and len(cols) == 6 and '.so' in cols[5]:
  410.                     lib = os.path.realpath(cols[5])
  411.                     libs.add(lib)
  412.  
  413.         # grab as much as we can
  414.         for l in libs:
  415.             if os.path.exists('/usr/lib/debug' + l):
  416.                 continue
  417.  
  418.             pkg = self.get_file_package(l, True)
  419.             if pkg:
  420.                 if not os.path.exists(l):
  421.                     if pkg in uninstallable:
  422.                         print >> sys.stderr, 'WARNING: %s cannot be installed (incompatible version)' % pkg
  423.                         continue
  424.                     if c.has_key(pkg):
  425.                         c[pkg].markInstall(False)
  426.                     else:
  427.                         print >> sys.stderr, 'WARNING: %s was loaded at runtime, but its package %s is not available' % (l, pkg)
  428.  
  429.                 if c.has_key(pkg+'-dbgsym') and pkg+'-dbgsym' not in uninstallable :
  430.                     c[pkg+'-dbgsym'].markInstall(False)
  431.                 else:
  432.                     print >> sys.stderr, 'WARNING: %s-dbgsym is not available or is incompatible' % pkg
  433.             else:
  434.                     print >> sys.stderr, 'WARNING: %s is needed, but cannot be mapped to a package' % l
  435.  
  436.         try:
  437.             if c.getChanges():
  438.                 os.environ['DEBIAN_FRONTEND'] = 'noninteractive'
  439.                 if unpack_only:
  440.                     self.fetch_unpack(c, fetchProgress, no_pkg, verbosity)
  441.                 else:
  442.                     c.commit(fetchProgress, installProgress)
  443.             installed += [p.name for p in c.getChanges()]
  444.         except (SystemError, IOError), e:
  445.             print >> sys.stderr, 'WARNING: could not install missing packages:', e
  446.             if os.geteuid() != 0:
  447.                 print >> sys.stderr, 'You either need to call this program as root or install these packages manually:'
  448.             for p in c.getChanges():
  449.                 print >> sys.stderr, '  %s %s' % (p.name, p.candidateVersion)
  450.  
  451.         return (installed, outdated)
  452.  
  453.     def remove_packages(self, packages, verbosity=0):
  454.         '''Remove packages.
  455.  
  456.         This is called after install_retracing_packages() to clean up again
  457.         afterwards. packages is a list of package names.
  458.         '''
  459.         if verbosity > 0:
  460.             so = sys.stderr
  461.         else:
  462.             so = subprocess.PIPE
  463.         subprocess.call(['dpkg', '-P'] + packages, stdout=so)
  464.  
  465.  
  466.     #
  467.     # Internal helper methods
  468.     #
  469.  
  470.     def _call_dpkg(self, args):
  471.         '''Call dpkg with given arguments and return output, or return None on
  472.         error.'''
  473.  
  474.         dpkg = subprocess.Popen(['dpkg'] + args, stdout=subprocess.PIPE,
  475.             stderr=subprocess.PIPE)
  476.         out = dpkg.communicate(input)[0]
  477.         if dpkg.returncode == 0:
  478.             return out
  479.         else:
  480.             raise ValueError, 'package does not exist'
  481.  
  482.     def _check_files_md5(self, sumfile):
  483.         '''Internal function for calling md5sum.
  484.  
  485.         This is separate from get_modified_files so that it is automatically
  486.         testable.'''
  487.  
  488.         if os.path.exists(sumfile):
  489.             m = subprocess.Popen(['/usr/bin/md5sum', '-c', sumfile],
  490.                 stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True,
  491.                 cwd='/', env={})
  492.             out = m.communicate()[0]
  493.         else:
  494.             m = subprocess.Popen(['/usr/bin/md5sum', '-c'],
  495.                 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
  496.                 stderr=subprocess.PIPE, close_fds=True, cwd='/', env={})
  497.             out = m.communicate(sumfile)[0]
  498.  
  499.         # if md5sum succeeded, don't bother parsing the output
  500.         if m.returncode == 0:
  501.             return []
  502.  
  503.         mismatches = []
  504.         for l in out.splitlines():
  505.             if l.endswith('FAILED'):
  506.                 mismatches.append(l.rsplit(':', 1)[0])
  507.  
  508.         return mismatches
  509.  
  510.     def _get_mirror(self):
  511.         '''Return the distribution mirror URL.
  512.  
  513.         If it has not been set yet, it will be read from the system
  514.         configuration.'''
  515.  
  516.         if not self._mirror:
  517.             for l in open('/etc/apt/sources.list'):
  518.                 fields = l.split()
  519.                 if len(fields) >= 3 and fields[0] == 'deb' and fields[1].startswith('http://'):
  520.                     self._mirror = fields[1]
  521.                     break
  522.             else:
  523.                 raise SystemError, 'cannot determine default mirror: /etc/apt/sources.list does not contain a valid deb line'
  524.  
  525.         return self._mirror
  526.  
  527.     def _search_contents(self, file, map_cachedir):
  528.         '''Internal function for searching file in Contents.gz.'''
  529.  
  530.         if map_cachedir:
  531.             dir = map_cachedir
  532.         else:
  533.             if not self._contents_dir:
  534.                 self._contents_dir = tempfile.mkdtemp()
  535.             dir = self._contents_dir
  536.  
  537.         arch = self.get_system_architecture()
  538.         map = os.path.join(dir, 'Contents-%s.gz' % arch)
  539.  
  540.         if not os.path.exists(map):
  541.             import urllib
  542.  
  543.             # determine distro release code name
  544.             lsb_release = subprocess.Popen(['lsb_release', '-sc'],
  545.                 stdout=subprocess.PIPE)
  546.             release_name = lsb_release.communicate()[0].strip()
  547.             assert lsb_release.returncode == 0
  548.  
  549.             url = '%s/dists/%s/Contents-%s.gz' % (self._get_mirror(), release_name, arch)
  550.             urllib.urlretrieve(url, map)
  551.             assert os.path.exists(map)
  552.  
  553.         if file.startswith('/'):
  554.             file = file[1:]
  555.  
  556.         # zgrep is magnitudes faster than a 'gzip.open/split() loop'
  557.         package = None
  558.         zgrep = subprocess.Popen(['zgrep', '-m1', '^%s[[:space:]]' % file, map],
  559.             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  560.         out = zgrep.communicate()[0]
  561.         # we do not check the return code, since zgrep -m1 often errors out
  562.         # with 'stdout: broken pipe'
  563.         if out:
  564.             package = out.split()[1].split(',')[0].split('/')[-1]
  565.  
  566.         return package
  567.  
  568.     def compare_versions(self, ver1, ver2):
  569.         '''Compare two package versions.
  570.  
  571.         Return -1 for ver < ver2, 0 for ver1 == ver2, and 1 for ver1 > ver2.'''
  572.  
  573.         return apt.VersionCompare(ver1, ver2)
  574.  
  575.     def enabled(self):
  576.         '''Return whether Apport should generate crash reports.
  577.  
  578.         Signal crashes are controlled by /proc/sys/kernel/core_pattern, but
  579.         some init script needs to set that value based on a configuration file.
  580.         This also determines whether Apport generates reports for Python,
  581.         package, or kernel crashes.
  582.         
  583.         Implementations should parse the configuration file which controls
  584.         Apport (such as /etc/default/apport in Debian/Ubuntu).
  585.         '''
  586.  
  587.         try:
  588.             conf = open(self.configuration).read()
  589.         except IOError:
  590.             # if the file does not exist, assume it's enabled
  591.             return True
  592.  
  593.         return re.search('^\s*enabled\s*=\s*0\s*$', conf, re.M) is None
  594.  
  595.     @classmethod
  596.     def deb_without_preinst(klass, deb):
  597.         '''Return .deb without a preinst script.
  598.  
  599.         If given .deb file has a preinst script, generate a <name>_noscript.deb
  600.         file without it and return that name; otherwise, return deb.
  601.         
  602.         If the modified deb already exists, its name is returned without recreating
  603.         it.
  604.         '''
  605.         ndeb = '/var/cache/apt/archives/%s_noscript%s' % os.path.splitext(os.path.basename(deb))
  606.  
  607.         if os.path.exists(ndeb):
  608.             return ndeb
  609.  
  610.         # get control.tar.gz    
  611.         ar = subprocess.Popen(['ar', 'p', deb, 'control.tar.gz'], stdout=subprocess.PIPE)
  612.         control_tar = ar.communicate()[0]
  613.         assert ar.returncode == 0
  614.  
  615.         # check if package has a preinst
  616.         tar = subprocess.Popen(['tar', 'tz', './preinst'], stdin=subprocess.PIPE,
  617.             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  618.         tar.communicate(control_tar)
  619.         if tar.returncode != 0:
  620.             return deb
  621.  
  622.         # unpack control.tar.gz and remove scripts
  623.         d = tempfile.mkdtemp()
  624.         d2 = tempfile.mkdtemp()
  625.         try:
  626.             tar = subprocess.Popen(['tar', '-C', d, '-xz'], stdin=subprocess.PIPE)
  627.             tar.communicate(control_tar)
  628.             assert tar.returncode == 0
  629.             for s in ('preinst', 'postinst', 'prerm', 'postrm'):
  630.                 path = os.path.join(d, s)
  631.                 if os.path.exists(path):
  632.                     os.unlink(path)
  633.  
  634.             control_tar_new = os.path.join(d2, 'control.tar.gz')
  635.             tar = subprocess.Popen(['tar', '-C', d, '-cz', '.'],
  636.                 stdin=subprocess.PIPE, stdout=open(control_tar_new, 'w'))
  637.             assert tar.wait() == 0
  638.  
  639.             shutil.copy(deb, ndeb)
  640.             r = subprocess.Popen(['ar', 'r', ndeb, control_tar_new])
  641.             assert r.wait() == 0
  642.         finally:
  643.             shutil.rmtree(d)
  644.             shutil.rmtree(d2)
  645.  
  646.         return ndeb
  647.  
  648.     @classmethod
  649.     def fetch_unpack(klass, cache, fetchProgress, no_dpkg=False, verbosity=0):
  650.         '''Fetch and unpack packages.
  651.         
  652.         The packages need to be marked for installation in the given
  653.         apt.Cache() object.
  654.         
  655.         fetchProgress must be a valid apt.progress.FetchProgress object.
  656.         '''
  657.         # fetch
  658.         fetcher = apt.apt_pkg.GetAcquire(fetchProgress)
  659.         pm = apt.apt_pkg.GetPackageManager(cache._depcache)
  660.         try:
  661.             res = cache._fetchArchives(fetcher, pm)
  662.         except IOError, e:
  663.             print >> sys.stderr, 'ERROR: could not fetch all archives:', e
  664.  
  665.         # extract
  666.         if verbosity:
  667.             so = sys.stderr
  668.         else:
  669.             so = subprocess.PIPE
  670.         if no_dpkg:
  671.             for i in fetcher.Items:
  672.                 if verbosity:
  673.                     print 'Extracting', i.DestFile
  674.                 if subprocess.call(['dpkg', '-x', i.DestFile, '/'], stdout=so,
  675.                     stderr=subprocess.STDOUT) != 0:
  676.                     print >> sys.stderr, 'WARNING: %s failed to extract' % i.DestFile
  677.         else:
  678.             res = subprocess.call(['dpkg', '--force-depends', '--force-overwrite', '--unpack'] + 
  679.                 [klass.deb_without_preinst(i.DestFile) for i in fetcher.Items], stdout=so)
  680.             if res != 0:
  681.                 raise IOError, 'dpkg failed to unpack archives'
  682.  
  683.         # remove other maintainer scripts
  684.         for c in cache.getChanges():
  685.             for script in ('postinst', 'prerm', 'postrm'):
  686.                 try:
  687.                     os.unlink('/var/lib/dpkg/info/%s.%s' % (c.name, script))
  688.                 except OSError:
  689.                     pass
  690.  
  691. impl = __AptDpkgPackageInfo()
  692.  
  693. #
  694. # Unit test
  695. #
  696.  
  697. if __name__ == '__main__':
  698.     import unittest, gzip
  699.  
  700.     class _AptDpkgPackageInfoTest(unittest.TestCase):
  701.  
  702.         def setUp(self):
  703.             # save and restore configuration file
  704.             self.orig_conf = impl.configuration
  705.  
  706.         def tearDown(self):
  707.             impl.configuration = self.orig_conf
  708.  
  709.         def test_check_files_md5(self):
  710.             '''_check_files_md5().'''
  711.  
  712.             td = tempfile.mkdtemp()
  713.             try:
  714.                 f1 = os.path.join(td, 'test 1.txt')
  715.                 f2 = os.path.join(td, 'test:2.txt')
  716.                 sumfile = os.path.join(td, 'sums.txt')
  717.                 open(f1, 'w').write('Some stuff')
  718.                 open(f2, 'w').write('More stuff')
  719.                 # use one relative and one absolute path in checksums file
  720.                 open(sumfile, 'w').write('''2e41290da2fa3f68bd3313174467e3b5  %s
  721.         f6423dfbc4faf022e58b4d3f5ff71a70  %s
  722.         ''' % (f1[1:], f2))
  723.                 self.assertEqual(impl._check_files_md5(sumfile), [], 'correct md5sums')
  724.  
  725.                 open(f1, 'w').write('Some stuff!')
  726.                 self.assertEqual(impl._check_files_md5(sumfile), [f1[1:]], 'file 1 wrong')
  727.                 open(f2, 'w').write('More stuff!')
  728.                 self.assertEqual(impl._check_files_md5(sumfile), [f1[1:], f2], 'files 1 and 2 wrong')
  729.                 open(f1, 'w').write('Some stuff')
  730.                 self.assertEqual(impl._check_files_md5(sumfile), [f2], 'file 2 wrong')
  731.  
  732.                 # check using a direct md5 list as argument
  733.                 self.assertEqual(impl._check_files_md5(open(sumfile).read()),
  734.                     [f2], 'file 2 wrong')
  735.  
  736.             finally:
  737.                 shutil.rmtree(td)
  738.  
  739.         def test_get_version(self):
  740.             '''get_version().'''
  741.  
  742.             self.assert_(impl.get_version('libc6').startswith('2'))
  743.             self.assertRaises(ValueError, impl.get_version, 'nonexisting')
  744.  
  745.         def test_get_available_version(self):
  746.             '''get_available_version().'''
  747.  
  748.             self.assert_(impl.get_available_version('libc6').startswith('2'))
  749.             self.assertRaises(ValueError, impl.get_available_version, 'nonexisting')
  750.  
  751.         def test_get_dependencies(self):
  752.             '''get_dependencies().'''
  753.  
  754.             # package with both Depends: and Pre-Depends:
  755.             d  = impl.get_dependencies('bash')
  756.             self.assert_(len(d) > 2)
  757.             self.assert_('libc6' in d)
  758.             for dep in d:
  759.                 self.assert_(impl.get_version(dep))
  760.  
  761.             # Pre-Depends: only
  762.             d  = impl.get_dependencies('coreutils')
  763.             self.assert_(len(d) >= 1)
  764.             self.assert_('libc6' in d)
  765.             for dep in d:
  766.                 self.assert_(impl.get_version(dep))
  767.  
  768.             # Depends: only
  769.             d  = impl.get_dependencies('libc6')
  770.             self.assert_(len(d) >= 1)
  771.             for dep in d:
  772.                 self.assert_(impl.get_version(dep))
  773.  
  774.         def test_get_source(self):
  775.             '''get_source().'''
  776.  
  777.             self.assertRaises(ValueError, impl.get_source, 'nonexisting')
  778.             self.assertEqual(impl.get_source('bash'), 'bash')
  779.             self.assertEqual(impl.get_source('libc6'), 'glibc')
  780.  
  781.         def test_is_distro_package(self):
  782.             '''is_distro_package().'''
  783.  
  784.             self.assertRaises(ValueError, impl.is_distro_package, 'nonexisting')
  785.             self.assert_(impl.is_distro_package('bash'))
  786.             # no False test here, hard to come up with a generic one
  787.  
  788.         def test_get_architecture(self):
  789.             '''get_architecture().'''
  790.  
  791.             self.assertRaises(ValueError, impl.get_architecture, 'nonexisting')
  792.             # just assume that bash uses the native architecture
  793.             d = subprocess.Popen(['dpkg', '--print-architecture'],
  794.                 stdout=subprocess.PIPE)
  795.             system_arch = d.communicate()[0].strip()
  796.             assert d.returncode == 0
  797.             self.assertEqual(impl.get_architecture('bash'), system_arch)
  798.  
  799.         def test_get_files(self):
  800.             '''get_files().'''
  801.  
  802.             self.assertRaises(ValueError, impl.get_files, 'nonexisting')
  803.             self.assert_('/bin/bash' in impl.get_files('bash'))
  804.  
  805.         def test_get_file_package(self):
  806.             '''get_file_package() on installed files.'''
  807.  
  808.             self.assertEqual(impl.get_file_package('/bin/bash'), 'bash')
  809.             self.assertEqual(impl.get_file_package('/bin/cat'), 'coreutils')
  810.             self.assertEqual(impl.get_file_package('/nonexisting'), None)
  811.  
  812.         def test_get_file_package_uninstalled(self):
  813.             '''get_file_package() on uninstalled packages.'''
  814.  
  815.             # determine distro release code name
  816.             lsb_release = subprocess.Popen(['lsb_release', '-sc'],
  817.                 stdout=subprocess.PIPE)
  818.             release_name = lsb_release.communicate()[0].strip()
  819.             assert lsb_release.returncode == 0
  820.  
  821.             # generate a test Contents.gz
  822.             basedir = tempfile.mkdtemp()
  823.             try:
  824.                 mapdir = os.path.join(basedir, 'dists', release_name)
  825.                 os.makedirs(mapdir)
  826.                 print >> gzip.open(os.path.join(mapdir, 'Contents-%s.gz' %
  827.                     impl.get_system_architecture()), 'w'), '''
  828.  foo header
  829. FILE                                                    LOCATION
  830. usr/bin/frobnicate                                      foo/frob
  831. usr/bin/frob                                            foo/frob-utils
  832. bo/gu/s                                                 na/mypackage
  833. '''
  834.  
  835.                 self.assertEqual(impl.get_file_package('usr/bin/frob', False, mapdir), None)
  836.                 # must not match frob (same file name prefix)
  837.                 self.assertEqual(impl.get_file_package('usr/bin/frob', True, mapdir), 'frob-utils')
  838.                 self.assertEqual(impl.get_file_package('/usr/bin/frob', True, mapdir), 'frob-utils')
  839.  
  840.                 # invalid mirror
  841.                 impl.set_mirror('file:///foo/nonexisting')
  842.                 self.assertRaises(IOError, impl.get_file_package, 'usr/bin/frob', True)
  843.  
  844.                 # valid mirror, no cache directory
  845.                 impl.set_mirror('file://' + basedir)
  846.                 self.assertEqual(impl.get_file_package('usr/bin/frob', True), 'frob-utils')
  847.                 self.assertEqual(impl.get_file_package('/usr/bin/frob', True), 'frob-utils')
  848.  
  849.                 # valid mirror, test caching
  850.                 cache_dir = os.path.join(basedir, 'cache')
  851.                 os.mkdir(cache_dir)
  852.                 self.assertEqual(impl.get_file_package('usr/bin/frob', True, cache_dir), 'frob-utils')
  853.                 self.assertEqual(len(os.listdir(cache_dir)), 1)
  854.                 self.assert_(os.listdir(cache_dir)[0].startswith('Contents-'))
  855.                 self.assertEqual(impl.get_file_package('/bo/gu/s', True, cache_dir), 'mypackage')
  856.             finally:
  857.                 shutil.rmtree(basedir)
  858.  
  859.         def test_get_file_package_diversion(self):
  860.             '''get_file_package() for a diverted file.'''
  861.  
  862.             # pick first diversion we have
  863.             p = subprocess.Popen('LC_ALL=C dpkg-divert --list | head -n 1',
  864.                 shell=True, stdout=subprocess.PIPE)
  865.             out = p.communicate()[0]
  866.             assert p.returncode == 0
  867.             assert out
  868.             fields = out.split()
  869.             file = fields[2]
  870.             pkg = fields[-1]
  871.  
  872.             self.assertEqual(impl.get_file_package(file), pkg)
  873.  
  874.         def test_get_system_architecture(self):
  875.             '''get_system_architecture().'''
  876.  
  877.             arch = impl.get_system_architecture()
  878.             # must be nonempty without line breaks
  879.             self.assertNotEqual(arch, '')
  880.             self.assert_('\n' not in arch)
  881.  
  882.         def test_compare_versions(self):
  883.             '''compare_versions.'''
  884.  
  885.             self.assertEqual(impl.compare_versions('1', '2'), -1)
  886.             self.assertEqual(impl.compare_versions('1.0-1ubuntu1', '1.0-1ubuntu2'), -1)
  887.             self.assertEqual(impl.compare_versions('1.0-1ubuntu1', '1.0-1ubuntu1'), 0)
  888.             self.assertEqual(impl.compare_versions('1.0-1ubuntu2', '1.0-1ubuntu1'), 1)
  889.             self.assertEqual(impl.compare_versions('1:1.0-1', '2007-2'), 1)
  890.             self.assertEqual(impl.compare_versions('1:1.0-1~1', '1:1.0-1'), -1)
  891.  
  892.         def test_enabled(self):
  893.             '''enabled.'''
  894.  
  895.             impl.configuration = '/nonexisting'
  896.             self.assertEqual(impl.enabled(), True)
  897.  
  898.             f = tempfile.NamedTemporaryFile()
  899.             impl.configuration = f.name
  900.             f.write('# configuration file\nenabled = 1')
  901.             f.flush()
  902.             self.assertEqual(impl.enabled(), True)
  903.             f.close()
  904.  
  905.             f = tempfile.NamedTemporaryFile()
  906.             impl.configuration = f.name
  907.             f.write('# configuration file\n  enabled =0  ')
  908.             f.flush()
  909.             self.assertEqual(impl.enabled(), False)
  910.             f.close()
  911.  
  912.             f = tempfile.NamedTemporaryFile()
  913.             impl.configuration = f.name
  914.             f.write('# configuration file\nnothing here')
  915.             f.flush()
  916.             self.assertEqual(impl.enabled(), True)
  917.             f.close()
  918.  
  919.         def test_get_kernel_pacakge(self):
  920.             '''get_kernel_package().'''
  921.  
  922.             self.assert_('linux' in impl.get_kernel_package())
  923.  
  924.     # only execute if dpkg is available
  925.     try:
  926.         if subprocess.call(['dpkg', '--help'], stdout=subprocess.PIPE,
  927.             stderr=subprocess.PIPE) == 0:
  928.             unittest.main()
  929.     except OSError:
  930.         pass
  931.